package org.datacoder.geocoding

import com.vividsolutions.jts.geom.*
import com.vividsolutions.jts.geom.impl.*
import org.datacoder.geocoding.datastructures.*
import org.datacoder.geocoding.datastructures.geoshapes.*
import org.datacoder.geocoding.jurisdiction.*

/**
 *  GeocodingService takes queries from the web layer
 *  (e.g., "What zip codes intersect St. Paul?"),
 *  converts the query object (e.g., St. Paul) into a
 *  rectangle or other simple shape, and passes the
 *  query on to the appropriate method of the
 *  EdelsbrunnerMaurerStructure.  It also filters the
 *  results returned from the EdelsbrunnerMaurerStructure
 *  to remove any false positives.
 */
class GeocodingService {

    public JurisdictionService jurisdictionService
    private GeometryFactory geometryFactory
    private EdelsbrunnerMaurerStructure emStructure
    private boolean transactional = false
    private static final double BUFFER_MARGIN = 0.001

    public void init(
        Collection<Jurisdiction> jurisdictions,
        Class ihiFinderClass,
        Class iviFinderClass,
        Class pwrFinderClass,
        Class rcpFinderClass
    ){
        this.geometryFactory = GeocodingUtils.geometryFactory
        emStructure = new EdelsbrunnerMaurerStructure(
            jurisdictions, ihiFinderClass, iviFinderClass, pwrFinderClass, rcpFinderClass
        )
    }

    /* NON-GEOCODING QUERIES */
    /*
     * These queries are not the subject of the thesis, but
     * are nevertheless important to have.
     */
    public Set<Jurisdiction> findByName(String name){
        Set<Jurisdiction> jurisdictions = Jurisdiction.findAllByLongName(name)
        jurisdictions.addAll(Jurisdiction.findAllByName(name))
        return jurisdictions
    }
    public Set<Jurisdiction> findChildJurisdictions(Jurisdiction juris){
        if(juris instanceof Country){
            return ((Country)juris).states
        }else if(juris instanceof State){
            State state = (State)juris
            SortedSet<Jurisdiction> childJurisdictions = new TreeSet<Jurisdiction>()
            childJurisdictions.addAll(state.congressionalDistricts)
            childJurisdictions.addAll(state.representativeDistricts)
            childJurisdictions.addAll(state.senatorialDistricts)
            childJurisdictions.addAll(Municipality.findAllByState(state))
            childJurisdictions.addAll(County.findAllByState(state))
            childJurisdictions.addAll(ZipCode.findAllByState(state))
            return childJurisdictions
        }else if(juris instanceof County){
            return VotingDistrict.findAllByCounty((County)juris)
        }else{
            return new HashSet<Jurisdiction>()
        }
    }
    public Point findCentroid(Jurisdiction juris){
        return juris.multiPolygon.getCentroid()
    }
    public Envelope findContainingRectangle(Set<Jurisdiction> jurisdictions){
        Geometry[] geometries = new Geometry[jurisdictions.size()]
        int i = 0
        for(juris in jurisdictions){
            geometries[i++] = juris.multiPolygon
        }
        GeometryCollection gc
        try{
            gc = new GeometryCollection(geometries, geometryFactory)
        }catch(Exception e){
            gc = null
        }
        Envelope envelope = null
        if(gc != null){
            envelope = gc.getEnvelopeInternal()
        }
        return envelope
    }

    /* GEOCODING QUERIES */
    /*
     * These methods handle entity-based geocoding queries.  They
     * fall into two categories: methods working with Jurisdictions
     * and methods working with Geometries from the Java Topology
     * Suite.  The Jurisdiction-based methods ultimately call the
     * geometry-based methods.
     */
    public Set<Jurisdiction> findJurisdictionsNearbyPoint(Point point, double miles){
        double lonRadius = GeocodingUtils.convertMilesToLon(point.y, miles)
        double latRadius = GeocodingUtils.convertMilesToLat(miles)
        Coordinate[] corners = new Coordinate[5]
        corners[0] = new Coordinate(point.x - lonRadius, point.y + latRadius)
        corners[1] = new Coordinate(point.x + lonRadius, point.y + latRadius)
        corners[2] = new Coordinate(point.x + lonRadius, point.y - latRadius)
        corners[3] = new Coordinate(point.x - lonRadius, point.y - latRadius)
        corners[4] = corners[0]
        Polygon searchRectangle = new Polygon(
            new LinearRing(new CoordinateArraySequence(corners), geometryFactory),
            null,
            geometryFactory
        )
        return findJurisdictionsIntersectingGeometry(searchRectangle)
    }

    public Set<Jurisdiction> findJurisdictionsContainingJurisdiction(Jurisdiction juris){
        Set<Jurisdiction> jurisdictions = findJurisdictionsContainingGeometry(juris.multiPolygon)
        jurisdictions.remove(juris)
        return jurisdictions
    }
    public Set<Jurisdiction> findJurisdictionsWithinJurisdiction(Jurisdiction juris){
        Set<Jurisdiction> jurisdictions = findJurisdictionsWithinGeometry(juris.multiPolygon)
        jurisdictions.remove(juris)
        return jurisdictions
    }
    public Set<Jurisdiction> findJurisdictionsIntersectingJurisdiction(Jurisdiction juris){
        Set<Jurisdiction> jurisdictions = findJurisdictionsIntersectingGeometry(juris.multiPolygon)
        jurisdictions.remove(juris)
        return jurisdictions
    }
    public Set<Jurisdiction> findJurisdictionsNearbyJurisdiction(Jurisdiction juris, double miles){
        Set<Jurisdiction> jurisdictions = findJurisdictionsNearbyGeometry(juris.multiPolygon, miles)
        jurisdictions.remove(juris)
        return jurisdictions
    }

    public Set<Jurisdiction> findJurisdictionsContainingGeometry(Geometry geometry){
        GeoPoint gp = GeocodingUtils.convertGeometryToGeoPoint(geometry)
        Set<Jurisdiction> jurisdictions = emStructure.findRectanglesContainingPoint(gp)
        Set<Jurisdiction> filteredJurisdictions = filterContainingJurisdictions(geometry, jurisdictions)
        return filteredJurisdictions
    }
    public Set<Jurisdiction> findJurisdictionsWithinGeometry(Geometry geometry){
        GeoRectangle gr = GeocodingUtils.convertGeometryToGeoRectangle(geometry)
        Set<Jurisdiction> jurisdictions = emStructure.findPointsWithinRectangle(gr)
        Set<Jurisdiction> filteredJurisdictions = filterWithinJurisdictions(geometry, jurisdictions)
        return filteredJurisdictions
    }
    public Set<Jurisdiction> findJurisdictionsIntersectingGeometry(Geometry geometry){
        GeoRectangle gr = GeocodingUtils.convertGeometryToGeoRectangle(geometry)
        Set<Jurisdiction> jurisdictions = emStructure.findRectanglesIntersectingRectangle(gr)
        Set<Jurisdiction> filteredJurisdictions = filterIntersectingJurisdictions(geometry, jurisdictions)
        return filteredJurisdictions
    }
    public Set<Jurisdiction> findJurisdictionsNearbyGeometry(Geometry geometry, double miles){
        Envelope envelope = geometry.getEnvelopeInternal()
        double minLon = envelope.getMinX()
        double maxLon = envelope.getMaxX()
        double minLat = envelope.getMinY()
        double maxLat = envelope.getMaxY()
        double lowerLonRadius = GeocodingUtils.convertMilesToLon(minLat, miles)
        double upperLonRadius = GeocodingUtils.convertMilesToLon(maxLat, miles)
        double latRadius = GeocodingUtils.convertMilesToLat(miles)
        Coordinate[] corners = new Coordinate[5]
        corners[0] = new Coordinate(minLon - upperLonRadius, maxLat + latRadius)
        corners[1] = new Coordinate(maxLon + upperLonRadius, maxLat + latRadius)
        corners[2] = new Coordinate(maxLon + lowerLonRadius, minLat - latRadius)
        corners[3] = new Coordinate(minLon - lowerLonRadius, minLat - latRadius)
        corners[4] = corners[0]
        Polygon searchRectangle = new Polygon(
            new LinearRing(new CoordinateArraySequence(corners), geometryFactory),
            null,
            geometryFactory
        )
        return findJurisdictionsIntersectingGeometry(searchRectangle)
    }

    /* FILTER METHODS */
    /*
     * The EdelsbrunnerMaurerStructure performs queries using rectangle
     * intersection, and thus can return false positives.  (For example,
     * the EdelsbrunnerMaurerStructure would erroneously report that
     * Milwaukee is in Michigan.)  We use these methods to detect and
     * remove these false positives.
     */
    private Set<Jurisdiction> filterContainingJurisdictions(Geometry geometry, Set<Jurisdiction> jurisdictions){
        Set<Jurisdiction> containingJurisdictions = new HashSet<Jurisdiction>()
        geometry = geometry.buffer(-BUFFER_MARGIN)
        for(juris in jurisdictions){
            if(juris.multiPolygon.contains(geometry)){
                containingJurisdictions.add(juris)
            }
        }
        return containingJurisdictions
    }
    private Set<Jurisdiction> filterWithinJurisdictions(Geometry geometry, Set<Jurisdiction> jurisdictions){
        Set<Jurisdiction> withinJurisdictions = new HashSet<Jurisdiction>()
        geometry = geometry.buffer(BUFFER_MARGIN)
        for(juris in jurisdictions){
            if(juris.multiPolygon.within(geometry)){
                withinJurisdictions.add(juris)
            }
        }
        return withinJurisdictions
    }
    private Set<Jurisdiction> filterIntersectingJurisdictions(Geometry geometry, Set<Jurisdiction> jurisdictions){
        Set<Jurisdiction> intersectingJurisdictions = new HashSet<Jurisdiction>()
        geometry = geometry.buffer(-BUFFER_MARGIN)
        for(juris in jurisdictions){
            try{
                if(juris.multiPolygon.intersects(geometry)){
                    intersectingJurisdictions.add(juris)
                }
            }catch(Exception e){
                if(juris.multiPolygon.buffer(0).intersects(geometry.buffer(0))){
                    intersectingJurisdictions.add(juris)
                }
            }
        }
        return intersectingJurisdictions
    }
}